﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using Clipboard=System.Windows.Clipboard;
using WebBrowser=System.Windows.Forms.WebBrowser;

namespace TypograFix
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    /// <summary>
    /// The tags in which text cannot be transformed.
    /// </summary>
    private static readonly string[] unsuitable = {"code", "pre", "script"};

    /// <summary>
    /// This stack stores a list of tags we're in.
    /// </summary>
    private static Stack<string> context;

    private readonly Timer updateTimer;
    private readonly WebBrowser webBrowser;
    private bool needsUpdate;

    public MainWindow()
    {
      InitializeComponent();
      propGrid.SelectedObject = App.ConversionOptions;
      webBrowser = new WebBrowser();
      winFormsHost.Child = webBrowser;
      updateTimer = new Timer {Interval = 5000};
      updateTimer.Tick += OnTick;
      updateTimer.Start();
      Closing += OnClosing;
      App.ConversionOptions.PropertyChanged += ConversionOptions_PropertyChanged;
    }

    void ConversionOptions_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      DoUpdate();
    }

    /// <summary>
    /// Returns <c>true</c> if replacement is forbidden in given context.
    /// </summary>
    /// <value><c>true</c> if cannot replace; otherwise, <c>false</c>.</value>
    private static bool cannotReplace
    {
      get
      {
        // code or pre elements get banned
        foreach (string s in context)
        {
          // can't even use LINQ :(
          if (Array.IndexOf(unsuitable, s.ToLower()) >= 0)
            return true;
        }

        return false;
      }
    }

    private void OnClosing(object sender, CancelEventArgs e)
    {
      updateTimer.Stop();
    }

    private void OnTick(object sender, EventArgs e)
    {
      if (needsUpdate)
      {
        DoUpdate();
      }
    }

    private void DoUpdate()
    {
      tbOut.Text = TypografixTransform(tbIn.Text, App.ConversionOptions);
      webBrowser.DocumentText = "<html><body>" + tbOut.Text + "</body></html>";
      needsUpdate = false;
    }

    private void tbIn_TextChanged(object sender, TextChangedEventArgs e)
    {
      needsUpdate = true;
    }

    // ------------- PRODUCTION ----------------------

    // todo: failure to maintain compliance with .net 2.0 for code below
    // will result in severe punishment.

    /// <summary>
    /// This function actually alters the text and makes the necessary changes.
    /// </summary>
    /// <param name="text">Initial text, with HTML entities decoded (use <see cref="T:System.Web.HttpUtility"/>).</param>
    /// <returns>The altered text.</returns>
    /// <remarks>Contains tracing until things work perfectly.</remarks>
    /// <exception cref="Exception">Will throw exception if source HTML is malformed.</exception>
    private static string TypografixTransform(string text, ConversionOptions options)
    {
      Trace.WriteLine("Transform(" + text + ")");
      HtmlBuilder hb = new HtmlBuilder();
      context = new Stack<string>();
      Stack<char> quoteStack = new Stack<char>();
      for (int i = 0; i < text.Length; ++i)
      {
        Trace.WriteLine("Met token '" + text[i] + "'");
        switch (text[i])
        {
            #region <

          case '<':
            hb.Append('<');
            StringBuilder tb = new StringBuilder();
            while (++i < text.Length)
            {
              hb.Append(text[i]);
              if (text[i] == '\"')
              {
                // this (opening quote) has been appended
                // append everything until we meet a closing one
                if (i+1 < text.Length)
                {
                  do
                  {
                    ++i;
                    hb.Append(text[i]);
                  } while (i < text.Length && text[i] != '\"');
                }
              }
              else if (char.IsWhiteSpace(text[i]))
              {
                // we're in this tag's context
                context.Push(tb.ToString());
                //tb = new StringBuilder();
                Debug.WriteLine("The string '" + tb + "' has been pushed, items count = " +
                                context.Count);
              }
              else if (text[i] == '/')
              {
                tb = new StringBuilder();
                string s = context.Pop();
                Debug.WriteLine("Popped " + s + " off context stack, items remaining = " + context.Count);
                // move until we meet > and add it
                while (++i < text.Length)
                {
                  if (text[i] == '>')
                    goto default;
                  hb.Append(text[i]);
                  Console.WriteLine("Appended internally " + text[i]);
                }
              }
              else if (text[i] == '>')
              {
                // if closed, break this loop
                context.Push(tb.ToString());
                //tb = new StringBuilder();
                Debug.WriteLine("The string '" + tb + "' has been pushed, items count = " +
                                context.Count);
                break;
              }
              else
                tb.Append(text[i]);
            }
            break;

            #endregion

            #region -

          case '-':
            if (cannotReplace) goto default;
            // if surrounded by spaces, it's an en dash
            if (i > 0 && i + 1 < text.Length && text[i - 1] == ' ' && text[i + 1] == ' ')
              hb.Append("&ndash;");
            else if (i + 1 < text.Length && text[i + 1] == '-' &&
                     (i + 2 == text.Length || text[i + 2] != '&'))
            {
              hb.Append("&mdash;");
              ++i; // ignore the second dash
            }
            else if (i + 5 < text.Length &&
                     text[i + 1] == '-' &&
                     text[i + 2] == '&' &&
                     text[i + 3] == 'g' &&
                     text[i + 4] == 't' &&
                     text[i + 5] == ';')
            {
              hb.Append("&rarr;");
              i += 5;
            }
            else goto default;
            // same for em dash
            break;

            #endregion

            #region (

          case '(':
            if (cannotReplace) goto default;
            if (i + 2 < text.Length && text[i + 2] == ')')
            {
              if (char.ToLower(text[i + 1]) == 'c')
              {
                hb.Append("&copy;");
                i += 2;
              }
              else if (char.ToLower(text[i + 1]) == 'r')
              {
                hb.Append("&reg;");
                i += 2;
              }
              else goto default;
            }
            else if (i + 3 < text.Length && char.ToLower(text[i + 1]) == 't'
                     && char.ToLower(text[i + 2]) == 'm'
                     && char.ToLower(text[i + 3]) == ')')
            {
              hb.Append("&#8482;");
              i += 3;
            }
            else goto default;
            break;

            #endregion

            #region '

          case '\'': // single quotes
            if (cannotReplace) goto default;

            // if there's no space on the right, force closing
            if (i > 0 && i + 1 < text.Length
                && (char.IsLetterOrDigit(text[i - 1]))
                && (char.IsLetterOrDigit(text[i + 1])))
            {
              hb.Append("&rsquo;"); // stack ignored
            }
            else if (quoteStack.Count > 0 && quoteStack.Peek() == '\'')
            {
              hb.Append("&rsquo;");
              quoteStack.Pop();
            }
            else
            {
              hb.Append("&lsquo;");
              quoteStack.Push('\'');
            }
            break;

            #endregion

            #region "

          case '\"': // double quotes
            Debug.WriteLine("Double quote!");
            if (cannotReplace) goto default;
            // if there is a quote on the stack, close it
            if (quoteStack.Count > 0 && quoteStack.Peek() == '\"')
            {
              hb.Append("&rdquo;");
              quoteStack.Pop();
            }
            else
            {
              hb.Append("&ldquo;");
              quoteStack.Push('\"');
            }
            break;

            #endregion

            #region .

          case '.': // possible ellipsis
            if (i + 2 < text.Length && text[i + 1] == '.' && text[i + 2] == '.')
            {
              if (i > 0 && i + 3 < text.Length)
              {
                char before = text[i - 1];
                char after = text[i + 3];
                if (Environment.NewLine.IndexOf(before) >= 0 &&
                    Environment.NewLine.IndexOf(after) >= 0)
                {
                  hb.Append("&#8942;");
                  i += 2;
                  continue;
                }
              }
              if (cannotReplace) goto default;
              hb.Append("&hellip;");
              i += 2;
            }
            else goto default;
            break;

            #endregion

            #region x

          case 'x':
            if (cannotReplace) goto default;
            if (i > 0 && char.IsDigit(text[i - 1])
                &&
                ((
                   i + 1 < text.Length &&
                   (char.IsDigit(text[i + 1]) || char.IsWhiteSpace(text[i + 1]) || text[i + 1] == '<')
                 ) || (i + 1 == text.Length))
              )
            {
              hb.Append("&times;");
            }
            else goto default;
            break;

            #endregion

            #region <

          case '&':
            if (cannotReplace) goto default;
            if (i + 5 < text.Length)
            {
              if (text[i + 1] == 'l' && text[i + 2] == 't' && text[i + 3] == ';')
              {
                if (text[i + 4] == '-' && text[i + 5] == '-')
                {
                  Debug.WriteLine("Single left arrow");
                  hb.Append("&larr;");
                  i += 5;
                }
                else if (text[i + 4] == '=' && text[i + 5] == '=')
                {
                  hb.Append("&lArr;");
                  i += 5;
                }
                else goto default;
              }
              else goto default;
            }
            else goto default;
            break;

            #endregion

          #region +
          case '+':
            if (cannotReplace || !options.ConvertPM || i + 1 == text.Length || text[i+1] != '-')
              goto default;
            else
            {
              hb.Append("&plusmn;");
              i++;
              break;
            }
          #endregion

          #region 1..9 0
          case '1':
          case '2':
          case '3':
          case '4':
          case '5':
          case '6':
          case '7':
          case '8':
          case '9':
          case '0':
            {
              // keep accumulating while digit
              int j;
              var acc = new StringBuilder();
              for (j = i; j < text.Length; ++j)
              {
                if (char.IsDigit(text[j]) || text[j] == ',')
                  acc.Append(text[j]);
                else break;
              }
              if (j + 1 >= text.Length)
                goto default;
              // st
              char lastChar = acc.ToString().ToCharArray().Last();
              if (lastChar == '1' && text[j] == 's' && text[j + 1] == 't')
              {
                hb.Append(acc + "<sup>st</sup>");
                i += acc.Length + 1;
              }
              else if (lastChar == '2' && text[j] == 'n' && text[j + 1] == 'd')
              {
                hb.Append(acc + "<sup>nd</sup>");
                i += acc.Length + 1;
              }
              else if (lastChar == '3' && text[j] == 'r' && text[j + 1] == 'd')
              {
                hb.Append(acc + "<sup>rd</sup>");
                i += acc.Length + 1;
              }
              else if (text[j] == 't' && text[j + 1] == 'h')
              {
                hb.Append(acc + "<sup>th</sup>");
                i += acc.Length + 1;
              }
              else goto default;
            }
            break;
          #endregion

          #region special symbols

          case '‘':
            if (cannotReplace) goto default;
            hb.Append("&lsquo;");
            break;

          case '’':
            if (cannotReplace) goto default;
            hb.Append("&rsquo;");
            break;

          case '“':
            if (cannotReplace) goto default;
            hb.Append("&ldquo;");
            break;

          case '”':
            if (cannotReplace) goto default;
            hb.Append("&rdquo;");
            break;

          #endregion

          default:
            Debug.WriteLine("Default appending " + text[i]);
            if (!char.IsControl(text[i]))
              hb.Append(text[i]);
            break;
        }
      }
      return hb.ToString();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
      Version version = Assembly.GetExecutingAssembly().GetName().Version;
      Title += string.Format(" v{0}.{1}", version.Major, version.Minor);
    }

    private void Window_Closed(object sender, EventArgs e)
    {
    }

    private void lnkPaste_Click(object sender, RoutedEventArgs e)
    {
      tbIn.Text = Clipboard.GetText();
    }

    private void lnkCopy_Click(object sender, RoutedEventArgs e)
    {
      DoCopy();
    }

    private void DoCopy()
    {
      try {
        DoUpdate();
        System.Windows.Forms.Clipboard.SetText(tbOut.Text);
      } catch
      {
        // Clipboard is the most unreliable class in existence
        // so don't be surprised if it didn't work
      }
    }

    private void lnkCopyAndMinimize_Click(object sender, RoutedEventArgs e)
    {
      DoCopy();
      WindowState = WindowState.Minimized;
    }
  }
}